using System;
using System.Collections.Generic;
using System.Linq;
using GLWrap.MathExt;
using GLWrap.SceneObjects;
using ReAnimation.Helpers.Common;
using ReAnimation.Helpers.WinForms;
using Tao.OpenGl;
using Tao.Platform.Windows;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Imaging;

namespace GLWrap
{
    public class GlSceneManager : IGlSceneManager
    {
        public float Angle { get; set; }
        public float Left { get; set; }
        public float Right { get; set; }
        public float Top { get; set; }
        public float Bottom { get; set; }
        public float Near { get; set; }
        public float Far { get; set; }
        public int Width { get; private set; }
        public int Height { get; private set; }
        public SimpleOpenGlControl OpenGlControl
        {
            get
            {
                return _openGlControl;
            }
        }

        private IDictionary<int, SceneObject> _sceneObjectsById;
        private bool _awaitingSelection;
        private int _xSelection, _ySelection;
        private Int32 _selectedId;
        private SimpleOpenGlControl _openGlControl;
        private bool _sceneObjectsChanged;

        public GlSceneManager(ICamera camera)
        {
            _sceneObjectsById = new Dictionary<int, SceneObject>();
            Camera = camera;
            Near = 0.001f;
            Far = 10000f;
            Angle = 45.0f;
            _sceneObjectsChanged = false;
            BackColor = Color.Black;
        }

        public void Initialize(SimpleOpenGlControl openGlControl)
        {
            InitOpenGlControl(openGlControl);

            int width = openGlControl.Width;
            int height = openGlControl.Height;

            Gl.glShadeModel(Gl.GL_SMOOTH);
            Gl.glEnable(Gl.GL_DEPTH_TEST);
            Gl.glEnable(Gl.GL_TEXTURE_2D);
            Gl.glEnable(Gl.GL_CULL_FACE);
            Gl.glEnable(Gl.GL_LIGHTING);
            Gl.glHint(Gl.GL_PERSPECTIVE_CORRECTION_HINT, Gl.GL_NICEST);

            Gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

            var size = (float)(Near * Math.Tan(Angle.DegreesToRadians() * 0.5));

            Gl.glMatrixMode(Gl.GL_PROJECTION);
            Gl.glLoadIdentity();
            Gl.glFrustum(Left = -size,
                         Right = size,
                         Bottom = -size / ((float)width / height),
                         Top = size / ((float)width / height),
                         Near,
                         Far);

            Gl.glViewport(0, 0, width, height);
            Width = width;
            Height = height;
            Gl.glMatrixMode(Gl.GL_MODELVIEW);
        }

        public Bitmap GrabScreenshot()
        {
            Bitmap bmp = new Bitmap(Width, Height);
            BitmapData data = bmp.LockBits(new Rectangle(0,0,Width,Height), ImageLockMode.WriteOnly,
                PixelFormat.Format24bppRgb);
            Gl.glReadPixels(0, 0, Width, Height, Gl.GL_BGR, Gl.GL_UNSIGNED_BYTE, data.Scan0);
            Gl.glFinish();
            bmp.UnlockBits(data);
            bmp.RotateFlip(RotateFlipType.RotateNoneFlipY);
            return bmp;
        }

        //TODO: implement Null object
        private void InitOpenGlControl(SimpleOpenGlControl openGlControl)
        {
            if (openGlControl == null) throw new ArgumentNullException("openGlControl");
            if (_openGlControl == openGlControl) return;

            if (_openGlControl != null)
            {
                _openGlControl.Paint -= OpenGlControlPaint;
                _openGlControl.Resize -= OpenGlControlResize;
                _openGlControl.MouseClick -= OpenGlControlMouseClick;
            }

            _openGlControl = openGlControl;
            _openGlControl.Paint += OpenGlControlPaint;
            _openGlControl.Resize += OpenGlControlResize;
            _openGlControl.MouseClick += OpenGlControlMouseClick;
            _openGlControl.MouseDown += new MouseEventHandler(_openGlControl_MouseDown);
            _openGlControl.MouseMove += new MouseEventHandler(_openGlControl_MouseMove);
            openGlControl.MouseUp += new MouseEventHandler(openGlControl_MouseUp);
        }

        void _openGlControl_MouseMove(object sender, MouseEventArgs e)
        {
            SceneMouseMove.Throw(this, new EventArg<Point>(e.Location));
        }

       
        void openGlControl_MouseUp(object sender, MouseEventArgs e)
        {
            SceneMouseUp.Throw(this, new EventArg<Point>(e.Location));
        }

        void _openGlControl_MouseDown(object sender, MouseEventArgs e)
        {
            SceneMouseDown.Throw(this, new EventArg<Point>(e.Location));
        }

        void OpenGlControlMouseClick(object sender, MouseEventArgs e)
        {
            CheckSelectionAt(e.X, e.Y);
        }

        void OpenGlControlResize(object sender, EventArgs e)
        {
            Initialize(_openGlControl);
        }

        private void OpenGlControlPaint(object sender, PaintEventArgs e)
        {
            Paint();
        }
        //TODO: refactor
        public void Paint()
        {
            bool await = _awaitingSelection;
            if (_awaitingSelection)
            {
                _awaitingSelection = false;
                Gl.glDisable(Gl.GL_LIGHTING);
                PhysicallyPaint();
                InternalCheckSelection();
                Gl.glEnable(Gl.GL_LIGHTING);
            }
            PhysicallyPaint();
            Gl.glFlush();
            if (await)
            {
                if (_sceneObjectsById.ContainsKey(_selectedId))
                {
                    var obj = _sceneObjectsById[_selectedId];

                    if (obj != null)
                    {
                        obj.HaveBeenSelected();
                        SceneObjectSelected.Throw(this, new EventArg<SceneObject>(obj));
                    }
                }
            }
        }

        public void Refresh()
        {
            _openGlControl.Refresh();
            
        }

        public IList<SceneObject> SceneObjects
        {
            get { return new List<SceneObject>(_sceneObjectsById.Values); }
        }

        private Color _backColor;
        public Color BackColor 
        {
            get { return _backColor; }
            set
            {
                _backColor = value;
                Gl.glClearColor(
                      (float)_backColor.R/255.0f,
                      (float)_backColor.G/255.0f,
                      (float)_backColor.B/255.0f, 1.0f);
            }
        }
        private ICamera _camera;
        public ICamera Camera 
        {
            get
            {
                return _camera;
            }
            set
            {
                if (_camera != value)
                    _camera = value;
                CameraChanged.Throw(this, EventArgs.Empty);
            }
        }

        public SceneObject GetObjectAtPoint(Point point)
        {
            _xSelection = point.X;
            _ySelection = point.Y;
            Gl.glDisable(Gl.GL_LIGHTING);
            PhysicallyPaint();
            InternalCheckSelection();
            Gl.glEnable(Gl.GL_LIGHTING);



            if (_sceneObjectsById.ContainsKey(_selectedId))
            {
                var obj = _sceneObjectsById[_selectedId];

                if (obj != null)
                {
                    return obj;
                }
            }
            else
            {
                foreach (SceneObject obj in _sceneObjectsById.Values)
                {
                    SceneObject so = ObjectWithId(obj,_selectedId);
                    if (so != null) return so;
                }
            }
            return null;
        }

        private SceneObject ObjectWithId(SceneObject obj,int id)
        {
            if (obj is SceneObjectsGroup)
            {
                foreach (SceneObject o in (obj as SceneObjectsGroup).AttachedObjects)
                {
                        SceneObject r = ObjectWithId(o, id);
                        if (r != null) return r;
                }
            }
            return obj.ObjectId == id ? obj : null;
        }

        //TODO: refactor
        private void PhysicallyPaint()
        {
            BackColor = BackColor;
            Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);
            Gl.glLoadIdentity();

            ApplyCamera();

            Gl.glTranslatef(0, 0, -1);
            
            IList<SceneObject> drawedObjects = new List<SceneObject>();
            IList<SceneObject> objectsToDraw = _sceneObjectsById.Values.ToList();

            int i = 0;
            while (i < objectsToDraw.Count)
            {
                _sceneObjectsChanged = false;
                drawedObjects.Add(objectsToDraw[i]);
                Gl.glPushMatrix();
                objectsToDraw[i++].Draw(this);
                Gl.glPopMatrix();

                if (_sceneObjectsChanged)
                {
                    objectsToDraw = _sceneObjectsById.Values.Except(drawedObjects).ToList();
                    i = 0;
                }
            }
        }

        private void ApplyCamera()
        {
            Glu.gluLookAt(
                Camera.Eye.X, Camera.Eye.Y, Camera.Eye.Z,
                Camera.View.X, Camera.View.Y, Camera.View.Z,
                Camera.Up.X, Camera.Up.Y, Camera.Up.Z
                );
        }

        public IGlSceneManager AddSceneObject(SceneObject sceneObject)
        {
            if (_sceneObjectsById.ContainsKey(sceneObject.ObjectId))
                return this;

            _sceneObjectsById.Add(sceneObject.ObjectId, sceneObject);
            sceneObject.AttachToScene(this);
            _sceneObjectsChanged = true;//????  ugly
            return this;
        }

        public IGlSceneManager RemoveSceneObject(SceneObject obj)
        {
            var keysToRemove = (from pair in _sceneObjectsById
                                where pair.Value.Equals(obj)
                                select pair.Key)
                                 .ToArray();

            foreach (var key in keysToRemove)
            {
                _sceneObjectsById[key].DetachFromScene(this);
                _sceneObjectsById.Remove(key);
                _sceneObjectsChanged = true;
            }

            return this;
        }

        public IGlSceneManager RemoveAllSceneObjects()
        {
            foreach (var sceneObject in _sceneObjectsById.Values)
                sceneObject.DetachFromScene(this);

            _sceneObjectsById.Clear();
            _sceneObjectsChanged = true;
            return this;
        }

        public bool IsAttached(SceneObject sceneObject)
        {
            return _sceneObjectsById.ContainsKey(sceneObject.ObjectId);
        }

        public event EventHandler<EventArg<SceneObject>> SceneObjectSelected;
        public event EventHandler<EventArg<Point>> SceneMouseDown;
        public event EventHandler<EventArg<Point>> SceneMouseUp;
        public event EventHandler<EventArg<Point>> SceneMouseMove;
        public event EventHandler CameraChanged;

        private void CheckSelectionAt(int x, int y)
        {
            _awaitingSelection = true;
            _xSelection = x;
            _ySelection = y;

            _openGlControl.Refresh();
        }

        private void InternalCheckSelection()
        {
            unsafe
            {
                Int32 components;
                Gl.glReadPixels(_xSelection, Height - _ySelection, 1, 1, Gl.GL_RGB, Gl.GL_UNSIGNED_BYTE,
                    new IntPtr(&components));

                
                _selectedId = components;
            }
        }
    }
}